TypeScript在类型检查方面非常强大,但有时,当某些类型是其他类型的子集且你又需要为它们定义类型检查时,TypeScript就会变得冗余。
举个例子,假设有两种响应类型:
x
interface UserProfileResponse { id: number; name: string; email: string; phone: string; avatar: string;}interface LoginResponse { id: number; name: string;}我们可以为UserProfileResponse定义类型并为LoginResponse选择属性,而不是定义相同上下文LoginResponse和UserProfileResponse的类型。
xxxxxxxxxxtype LoginResponse = Pick<UserProfileResponse, "id" | "name">;下面我们将介绍一些可以帮助编写更佳代码的实用程序函数。
构建一个类型,定义其属性全是小写。然后使用Uppercase方法将其全部转化为大写。
xxxxxxxxxxtype Role = "admin" | "user" | "guest";// 不好的做法 💩type UppercaseRole = "ADMIN" | "USER" | "GUEST";// 正确的做法 ✅type UppercaseRole = Uppercase<Role>; // "ADMIN" | "USER" | "GUEST"与前面一个例子相反,先构建一个属性全是大写的类型,然后使用Lowercase方法将其全部转化为小写。
xxxxxxxxxxtype Role = "ADMIN" | "USER" | "GUEST";// 不好的做法 💩type LowercaseRole = "admin" | "user" | "guest";// 正确的做法 ✅type LowercaseRole = Lowercase<Role>; // "admin" | "user" | "guest"将所有属性的首字母大写。
xxxxxxxxxxtype Role = "admin" | "user" | "guest";// 不好的做法 💩type CapitalizeRole = "Admin" | "User" | "Guest";// 正确的做法 ✅type CapitalizeRole = Capitalize<Role>; // "Admin" | "User" | "Guest"与Capitalize相反,将所有属性取消首字母大写。
xxxxxxxxxxtype Role = "Admin" | "User" | "Guest";// 不好的做法 💩type UncapitalizeRole = "admin" | "user" | "guest";// 正确的做法 ✅type UncapitalizeRole = Uncapitalize<Role>; // "admin" | "user" | "guest"将定义的接口中的属性设置为可选属性。
xxxxxxxxxxinterface User { name: string; age: number; password: string;}// 不好的做法 💩interface PartialUser { name?: string; age?: number; password?: string;}// 正确的做法 ✅type PartialUser = Partial<User>;与Partial相反,Required可以将接口中的可选属性设置为必需。
xxxxxxxxxxinterface User { name?: string; age?: number; password?: string;}// 不好的做法 💩interface RequiredUser { name: string; age: number; password: string;}// 正确的做法 ✅type RequiredUser = Required<User>;将属性设置为只读。
xxxxxxxxxxinterface User { role: string;}// 不好的做法 💩const user: User = { role: "ADMIN" };user.role = "USER";// 正确的做法 ✅type ReadonlyUser = Readonly<User>;const user: ReadonlyUser = { role: "ADMIN" };user.role = "USER"; // 将属性'role'设置为只读后,再次对'role'设值就会报错构造一个对象类型,其属性key是Keys,属性value是Tpye。被用于映射一个类型的属性到另一个类型。
xxxxxxxxxxinterface Address { street: string; pin: number;}interface Addresses { home: Address; office: Address;}// Alternative ✅type AddressesRecord = Record<"home" | "office", Address>;就是从一个复合类型中,取出几个想要的类型的组合,例如:
xxxxxxxxxxinterface User { name: string; age: number; password: string;}// 不好的做法 💩interface UserPartial { name: string; age: number;}// 正确的做法 ✅type UserPartial = Pick<User, "name" | "age">;Omit是TypeScript3.5新增的一个辅助类型,它的作用主要是:以一个类型为基础支持剔除某些属性,然后返回一个新类型。
xxxxxxxxxxinterface User { name: string; age: number; password: string;}// 不好的做法 💩interface UserPartial { name: string; age: number;}// 正确的做法 ✅type UserPartial = Omit<User, "password">;将类型中其中一些属性排除,并创建排除属性后的新类型。
xxxxxxxxxxtype Role = "ADMIN" | "USER" | "GUEST";// 不好的做法 💩type NonAdminRole = "USER" | "GUEST";// 正确的做法 ✅type NonAdmin = Exclude<Role, "ADMIN">; // "USER" | "GUEST"它通过从可分配给联合的类型中提取所有联合成员来创建新类型。
xxxxxxxxxxtype Role = "ADMIN" | "USER" | "GUEST";// 不好的做法 💩type AdminRole = "ADMIN";// 正确的做法 ✅type Admin = Extract<Role, "ADMIN">; // "ADMIN"通过从类型中排除null和undefined来创建新类型。
xxxxxxxxxxtype Role = "ADMIN" | "USER" | null;// 不好的做法 💩type NonNullableRole = "ADMIN" | "USER";// 正确的做法 ✅type NonNullableRole = NonNullable<Role>; // "ADMIN" | "USER"
例子很简单,但是必须把简单的例子记住,这就不简单了。
keyof作用:从一个对象类型中提取所有键名,组成一个字符串字面量联合类型。
例子:
xxxxxxxxxxtype UserKey = keyof User; // 'id' | 'name' | 'email'→ 说明:UserKey 只能是 User 这个类型里真正存在的字段名,防止写错属性名(比如写成 "names" 会报错)。
xxxxxxxxxxfunction getProp<T, K extends keyof T>(obj: T, key: K): T[K]→ 说明:约束第二个参数 key 必须是 T 真正存在的键,保证运行时 obj[key] 一定能取到值且类型安全。
[](索引访问类型)作用:通过键(可以是字面量或联合类型)从对象类型中取出对应属性的类型。
例子:
xxxxxxxxxxtype PrimaryColor = Colors["primary"]; // "#007bff"→ 说明:精确地取出 primary 这个具体键对应的类型(这里是字面量类型 "#007bff"),而不是更宽泛的 string。
xxxxxxxxxxtype PropType = User[keyof User]; // number | string | boolean→ 说明:表示“User 对象所有可能的值的类型集合”,常用来描述“任意字段的值”。
extends ? :(条件类型)作用:像三元表达式一样,根据类型是否满足某个条件来选择不同的结果类型。
例子:
好的,我明白你觉得之前的例子太抽象、太“类型编程”了,看起来像谜语一样难懂。
我现在重新给你举 extends 的例子,这次全部用最简单、最日常的场景,像写普通代码一样解释清楚。每个例子都告诉你:
extends 到底在起什么作用(用大白话)extends 会怎样(为什么需要它)例子1:最简单、最常见的 —— “这个参数必须有 length 属性”
xxxxxxxxxxfunction printLength<T extends { length: number }>(thing: T) { console.log(thing.length); // 这里放心用 length,不会报错}printLength("hello"); // OK,字符串有 lengthprintLength([1, 2, 3]); // OK,数组有 lengthprintLength(123); // 报错!数字没有 lengthextends 在干什么?
它在告诉 TypeScript:“T 这个类型必须长得像有 length 属性的样子”,不然不让传进来。
没有 extends { length: number },你传数字进来时,thing.length 会直接报错(因为 number 没 length)。
例子2:取对象属性的安全函数(超级常用)
xxxxxxxxxxfunction getValue<T, K extends keyof T>(obj: T, key: K) { return obj[key]; // 类型安全,返回值是 obj[key] 的真实类型}const person = { name: "小明", age: 18, isStudent: true };const name = getValue(person, "name"); // stringconst age = getValue(person, "age"); // numbergetValue(person, "height"); // 报错!没有 height 这个属性extends 在干什么?K extends keyof T 意思是:key 必须是 person 真正存在的属性名。
如果没有这个 extends,你写 getValue(person, "随便乱写") 也不会报错,但运行时 obj["随便乱写"] 就是 undefined,很危险。
例子3:接口继承(写组件 props 时最常见)
xxxxxxxxxxinterface ButtonProps { text: string; onClick: () => void;}interface FancyButtonProps extends ButtonProps { color: "red" | "blue"; size: "small" | "large";}function FancyButton(props: FancyButtonProps) { // 既有 text、onClick,也有 color、size}extends 在干什么?
让 FancyButtonProps 自动拥有 ButtonProps 里所有的属性,再额外加自己的。
相当于 FancyButtonProps = { text, onClick, color, size },不用重复写。
例子4:判断是不是字符串(最基础的条件类型)
xxxxxxxxxxtype Check<T> = T extends string ? "这是字符串" : "不是字符串";type Test1 = Check<"hello">; // "这是字符串"type Test2 = Check<999>; // "不是字符串"extends 在干什么? 像 if 判断:如果 T 是 string,就选左边;否则选右边。 这就是“带 ? :”的经典用法。
例子5:去掉函数类型(过滤掉函数)
xxxxxxxxxxtype OnlyData<T> = T extends Function ? never : T;type Mixed = string | number | (() => void) | boolean;type Clean = OnlyData<Mixed>; // string | number | boolean (函数被踢掉了)extends 在干什么? 检查 T 是不是函数:如果是,就返回 never(相当于删除);不是就保留。
总结:你最常遇到的两种 extends(记住这两种就够用了)
不带 ? : 的(约束)
<T extends XXX> 或 interface Y extends X 带 ? : 的(条件判断)
T extends XXX ? 是 : 否 infer(类型推断)作用:在条件类型中“猜”出(捕获)某个位置的未知类型,把它绑定到一个变量上供后面使用。
例子:
xxxxxxxxxxtype ElementType<T> = T extends (infer U)[] ? U : never;→ 说明:infer U 让 TypeScript 自动推断出数组里元素的真实类型(比如 number[] → infer U 得到 number),然后把 U 作为结果返回。
xxxxxxxxxxtype MyReturnType<T> = T extends (args: any[]) => infer R ? R : never;→ 说明:捕获函数返回值的类型(R),实现类似官方 ReturnType 的功能。
in(映射类型中的遍历)作用:遍历一个联合类型(通常是 keyof 出来的),为每个成员生成一个属性。
例子:
xxxxxxxxxxtype StatusFlags = { [K in Keys]: boolean };→ 说明:把 Keys 里的每个字符串('loading'|'success'|'error')都变成一个属性名,并且每个属性值都是 boolean 类型,快速生成一组同构的布尔字段。
`xxx${T}yyy`作用:在类型层面进行字符串拼接、组合、转换,生成新的字符串字面量联合类型。
例子:
xxxxxxxxxxtype HandlerName = `on${Capitalize<Event>}`;→ 说明:把 Event 里的每个值(click → Click)首字母大写后拼接上 "on",生成规范的事件处理器名称类型,用于类型安全的字符串匹配。
satisfies(TS 4.9+)作用:对值进行类型检查(必须满足某个类型),但不收窄/丢失原有的更精确的字面量类型。
例子:
xxxxxxxxxxconst theme = { colors: { primary: "#0070f3", } } satisfies { colors: Record<string, string>; };→ 说明:强制要求 theme 必须符合后面的形状(有 colors 对象等),但保留了 "#0070f3" 这样的具体字面量类型,而不是退化为宽泛的 string。
不要将satisfies和extends搞混了。虽然中文翻译都好像是“是否满足”,但作用是完全不同的。它们根本不是同一类东西,作用的层面和目的完全不同。简单一句话总结:
- extends:类型层面的关系定义和约束(在类型系统里说“A 是 B 的子类型”或“必须符合 B”)
- satisfies:值层面的类型校验(在写具体值/对象/表达式时,说“这个值必须能赋值给某个类型,但别把它的具体类型变宽/变模糊”)
层级不同:一个在“类型世界”,一个在“值世界”
xxxxxxxxxx// extends → 只出现在类型定义/泛型约束里(类型世界)type Animal = { eat(): void };interface Dog extends Animal { bark(): void } // Dog “继承/扩展” Animal(类型关系)function fn<T extends { name: string }>(obj: T) { // 约束 T 必须有 nameconsole.log(obj.name);}xxxxxxxxxx// satisfies → 只出现在具体值/表达式后面(值世界)const config = {api: "https://example.com",timeout: 5000 as const, // 保持 5000 这个字面量retry: true} satisfies {api: string;timeout: number;retry: boolean;// 可选:可以少写点属性,但不能多写或类型错};
- extends:你在定义类型时用,告诉 TypeScript “这个类型必须长这样或包含那些东西”。
- satisfies:你在写 JavaScript 值时用,告诉 TypeScript “检查一下这个对象/值符不符合要求,但别把它的类型变宽”。
is(类型谓词 / 类型守卫)作用:告诉 TypeScript 在某个条件为 true 时,变量的类型可以被收窄成更具体的类型。
例子:
xxxxxxxxxxfunction isString(value: unknown): value is string { }if (isString(input)) { input.toUpperCase() }→ 说明:在 if 块内部,input 被 TypeScript 自动收窄为 string,从而允许调用字符串专属的方法,而不会报错。